/*
 * Copyright 2002-2007 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.beans;

import java.beans.PropertyDescriptor;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.List;

import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;

/**
 * Static convenience methods for JavaBeans: for instantiating beans, checking
 * bean property types, copying bean properties, etc.
 * 
 * <p>
 * Mainly for use within the framework, but to some degree also useful for
 * application classes.
 * 
 * @author Rod Johnson
 * @author Juergen Hoeller
 * @author Rob Harrop
 */
public abstract class BeanUtils {

	/**
	 * Convenience method to instantiate a class using its no-arg constructor.
	 * As this method doesn't try to load classes by name, it should avoid
	 * class-loading issues.
	 * <p>
	 * Note that this method tries to set the constructor accessible if given a
	 * non-accessible (that is, non-public) constructor.
	 * 
	 * @param clazz
	 *            class to instantiate
	 * @return the new instance
	 * @throws BeanInstantiationException
	 *             if the bean cannot be instantiated
	 */
	public static Object instantiateClass(Class clazz)
			throws BeanInstantiationException {
		Assert.notNull(clazz, "Class must not be null");
		if (clazz.isInterface()) {
			throw new BeanInstantiationException(clazz,
					"Specified class is an interface");
		}
		try {
			return instantiateClass(
					clazz.getDeclaredConstructor((Class[]) null), null);
		} catch (NoSuchMethodException ex) {
			throw new BeanInstantiationException(clazz,
					"No default constructor found", ex);
		}
	}

	/**
	 * Convenience method to instantiate a class using the given constructor. As
	 * this method doesn't try to load classes by name, it should avoid
	 * class-loading issues.
	 * <p>
	 * Note that this method tries to set the constructor accessible if given a
	 * non-accessible (that is, non-public) constructor.
	 * 
	 * @param ctor
	 *            the constructor to instantiate
	 * @param args
	 *            the constructor arguments to apply
	 * @return the new instance
	 * @throws BeanInstantiationException
	 *             if the bean cannot be instantiated
	 */
	public static Object instantiateClass(Constructor ctor, Object[] args)
			throws BeanInstantiationException {
		Assert.notNull(ctor, "Constructor must not be null");
		try {
			if (!Modifier.isPublic(ctor.getModifiers())
					|| !Modifier.isPublic(ctor.getDeclaringClass()
							.getModifiers())) {
				ctor.setAccessible(true);
			}
			return ctor.newInstance(args);
		} catch (InstantiationException ex) {
			throw new BeanInstantiationException(ctor.getDeclaringClass(),
					"Is it an abstract class?", ex);
		} catch (IllegalAccessException ex) {
			throw new BeanInstantiationException(
					ctor.getDeclaringClass(),
					"Has the class definition changed? Is the constructor accessible?",
					ex);
		} catch (IllegalArgumentException ex) {
			throw new BeanInstantiationException(ctor.getDeclaringClass(),
					"Illegal arguments for constructor", ex);
		} catch (InvocationTargetException ex) {
			throw new BeanInstantiationException(ctor.getDeclaringClass(),
					"Constructor threw exception", ex.getTargetException());
		}
	}

	/**
	 * Find a method with the given method name and the given parameter types,
	 * declared on the given class or one of its superclasses. Prefers public
	 * methods, but will return a protected, package access, or private method
	 * too.
	 * <p>
	 * Checks <code>Class.getMethod</code> first, falling back to
	 * <code>findDeclaredMethod</code>. This allows to find public methods
	 * without issues even in environments with restricted Java security
	 * settings.
	 * 
	 * @param clazz
	 *            the class to check
	 * @param methodName
	 *            the name of the method to find
	 * @param paramTypes
	 *            the parameter types of the method to find
	 * @return the Method object, or <code>null</code> if not found
	 * @see java.lang.Class#getMethod
	 * @see #findDeclaredMethod
	 */
	public static Method findMethod(Class clazz, String methodName,
			Class[] paramTypes) {
		try {
			return clazz.getMethod(methodName, paramTypes);
		} catch (NoSuchMethodException ex) {
			return findDeclaredMethod(clazz, methodName, paramTypes);
		}
	}

	/**
	 * Find a method with the given method name and the given parameter types,
	 * declared on the given class or one of its superclasses. Will return a
	 * public, protected, package access, or private method.
	 * <p>
	 * Checks <code>Class.getDeclaredMethod</code>, cascading upwards to all
	 * superclasses.
	 * 
	 * @param clazz
	 *            the class to check
	 * @param methodName
	 *            the name of the method to find
	 * @param paramTypes
	 *            the parameter types of the method to find
	 * @return the Method object, or <code>null</code> if not found
	 * @see java.lang.Class#getDeclaredMethod
	 */
	public static Method findDeclaredMethod(Class clazz, String methodName,
			Class[] paramTypes) {
		try {
			return clazz.getDeclaredMethod(methodName, paramTypes);
		} catch (NoSuchMethodException ex) {
			if (clazz.getSuperclass() != null) {
				return findDeclaredMethod(clazz.getSuperclass(), methodName,
						paramTypes);
			}
			return null;
		}
	}

	/**
	 * Find a method with the given method name and minimal parameters (best
	 * case: none), declared on the given class or one of its superclasses.
	 * Prefers public methods, but will return a protected, package access, or
	 * private method too.
	 * <p>
	 * Checks <code>Class.getMethods</code> first, falling back to
	 * <code>findDeclaredMethodWithMinimalParameters</code>. This allows to find
	 * public methods without issues even in environments with restricted Java
	 * security settings.
	 * 
	 * @param clazz
	 *            the class to check
	 * @param methodName
	 *            the name of the method to find
	 * @return the Method object, or <code>null</code> if not found
	 * @throws IllegalArgumentException
	 *             if methods of the given name were found but could not be
	 *             resolved to a unique method with minimal parameters
	 * @see java.lang.Class#getMethods
	 * @see #findDeclaredMethodWithMinimalParameters
	 */
	public static Method findMethodWithMinimalParameters(Class clazz,
			String methodName) throws IllegalArgumentException {

		Method targetMethod = doFindMethodWithMinimalParameters(
				clazz.getDeclaredMethods(), methodName);
		if (targetMethod == null) {
			return findDeclaredMethodWithMinimalParameters(clazz, methodName);
		}
		return targetMethod;
	}

	/**
	 * Find a method with the given method name and minimal parameters (best
	 * case: none), declared on the given class or one of its superclasses. Will
	 * return a public, protected, package access, or private method.
	 * <p>
	 * Checks <code>Class.getDeclaredMethods</code>, cascading upwards to all
	 * superclasses.
	 * 
	 * @param clazz
	 *            the class to check
	 * @param methodName
	 *            the name of the method to find
	 * @return the Method object, or <code>null</code> if not found
	 * @throws IllegalArgumentException
	 *             if methods of the given name were found but could not be
	 *             resolved to a unique method with minimal parameters
	 * @see java.lang.Class#getDeclaredMethods
	 */
	public static Method findDeclaredMethodWithMinimalParameters(Class clazz,
			String methodName) throws IllegalArgumentException {

		Method targetMethod = doFindMethodWithMinimalParameters(
				clazz.getDeclaredMethods(), methodName);
		if (targetMethod == null && clazz.getSuperclass() != null) {
			return findDeclaredMethodWithMinimalParameters(
					clazz.getSuperclass(), methodName);
		}
		return targetMethod;
	}

	/**
	 * Find a method with the given method name and minimal parameters (best
	 * case: none) in the given list of methods.
	 * 
	 * @param methods
	 *            the methods to check
	 * @param methodName
	 *            the name of the method to find
	 * @return the Method object, or <code>null</code> if not found
	 * @throws IllegalArgumentException
	 *             if methods of the given name were found but could not be
	 *             resolved to a unique method with minimal parameters
	 */
	private static Method doFindMethodWithMinimalParameters(Method[] methods,
			String methodName) throws IllegalArgumentException {

		Method targetMethod = null;
		int numMethodsFoundWithCurrentMinimumArgs = 0;
		for (int i = 0; i < methods.length; i++) {
			if (methods[i].getName().equals(methodName)) {
				int numParams = methods[i].getParameterTypes().length;
				if (targetMethod == null
						|| numParams < targetMethod.getParameterTypes().length) {
					targetMethod = methods[i];
					numMethodsFoundWithCurrentMinimumArgs = 1;
				} else {
					if (targetMethod.getParameterTypes().length == numParams) {
						// Additional candidate with same length.
						numMethodsFoundWithCurrentMinimumArgs++;
					}
				}
			}
		}
		if (numMethodsFoundWithCurrentMinimumArgs > 1) {
			throw new IllegalArgumentException(
					"Cannot resolve method '"
							+ methodName
							+ "' to a unique method. Attempted to resolve to overloaded method with "
							+ "the least number of parameters, but there were "
							+ numMethodsFoundWithCurrentMinimumArgs
							+ " candidates.");
		}
		return targetMethod;
	}

	/**
	 * Parse a method signature in the form
	 * <code>methodName[([arg_list])]</code>, where <code>arg_list</code> is an
	 * optional, comma-separated list of fully-qualified type names, and
	 * attempts to resolve that signature against the supplied
	 * <code>Class</code>.
	 * <p>
	 * When not supplying an argument list (<code>methodName</code>) the method
	 * whose name matches and has the least number of parameters will be
	 * returned. When supplying an argument type list, only the method whose
	 * name and argument types match will be returned.
	 * <p>
	 * Note then that <code>methodName</code> and <code>methodName()</code> are
	 * <strong>not</strong> resolved in the same way. The signature
	 * <code>methodName</code> means the method called <code>methodName</code>
	 * with the least number of arguments, whereas <code>methodName()</code>
	 * means the method called <code>methodName</code> with exactly 0 arguments.
	 * <p>
	 * If no method can be found, then <code>null</code> is returned.
	 * 
	 * @param signature
	 *            the method signature as String representation
	 * @param clazz
	 *            the class to resolve the method signature against
	 * @return the resolved Method
	 * @see #findMethod
	 * @see #findMethodWithMinimalParameters
	 */
	public static Method resolveSignature(String signature, Class clazz) {
		Assert.hasText(signature, "'signature' must not be empty");
		Assert.notNull(clazz, "Class must not be null");

		int firstParen = signature.indexOf("(");
		int lastParen = signature.indexOf(")");

		if (firstParen > -1 && lastParen == -1) {
			throw new IllegalArgumentException("Invalid method signature '"
					+ signature + "': expected closing ')' for args list");
		} else if (lastParen > -1 && firstParen == -1) {
			throw new IllegalArgumentException("Invalid method signature '"
					+ signature + "': expected opening '(' for args list");
		} else if (firstParen == -1 && lastParen == -1) {
			return findMethodWithMinimalParameters(clazz, signature);
		} else {
			String methodName = signature.substring(0, firstParen);
			String[] parameterTypeNames = StringUtils
					.commaDelimitedListToStringArray(signature.substring(
							firstParen + 1, lastParen));
			Class[] parameterTypes = new Class[parameterTypeNames.length];
			for (int i = 0; i < parameterTypeNames.length; i++) {
				String parameterTypeName = parameterTypeNames[i].trim();
				try {
					parameterTypes[i] = ClassUtils.forName(parameterTypeName,
							clazz.getClassLoader());
				} catch (Throwable ex) {
					throw new IllegalArgumentException(
							"Invalid method signature: unable to resolve type ["
									+ parameterTypeName + "] for argument " + i
									+ ". Root cause: " + ex);
				}
			}
			return findMethod(clazz, methodName, parameterTypes);
		}
	}

	/**
	 * Retrieve the JavaBeans <code>PropertyDescriptor</code>s of a given class.
	 * 
	 * @param clazz
	 *            the Class to retrieve the PropertyDescriptors for
	 * @return an array of <code>PropertyDescriptors</code> for the given class
	 * @throws BeansException
	 *             if PropertyDescriptor look fails
	 */
	public static PropertyDescriptor[] getPropertyDescriptors(Class clazz)
			throws BeansException {
		CachedIntrospectionResults cr = CachedIntrospectionResults
				.forClass(clazz);
		return cr.getBeanInfo().getPropertyDescriptors();
	}

	/**
	 * Retrieve the JavaBeans <code>PropertyDescriptors</code> for the given
	 * property.
	 * 
	 * @param clazz
	 *            the Class to retrieve the PropertyDescriptor for
	 * @param propertyName
	 *            the name of the property
	 * @return the corresponding PropertyDescriptor, or <code>null</code> if
	 *         none
	 * @throws BeansException
	 *             if PropertyDescriptor lookup fails
	 */
	public static PropertyDescriptor getPropertyDescriptor(Class clazz,
			String propertyName) throws BeansException {

		CachedIntrospectionResults cr = CachedIntrospectionResults
				.forClass(clazz);
		return cr.getPropertyDescriptor(propertyName);
	}

	/**
	 * Find a JavaBeans <code>PropertyDescriptor</code> for the given method,
	 * with the method either being the read method or the write method for that
	 * bean property.
	 * 
	 * @param method
	 *            the method to find a corresponding PropertyDescriptor for
	 * @return the corresponding PropertyDescriptor, or <code>null</code> if
	 *         none
	 * @throws BeansException
	 *             if PropertyDescriptor lookup fails
	 */
	public static PropertyDescriptor findPropertyForMethod(Method method)
			throws BeansException {
		Assert.notNull(method, "Method must not be null");
		PropertyDescriptor[] pds = getPropertyDescriptors(method
				.getDeclaringClass());
		for (int i = 0; i < pds.length; i++) {
			PropertyDescriptor pd = pds[i];
			if (method.equals(pd.getReadMethod())
					|| method.equals(pd.getWriteMethod())) {
				return pd;
			}
		}
		return null;
	}

	/**
	 * Determine the bean property type for the given property from the given
	 * classes/interfaces, if possible.
	 * 
	 * @param propertyName
	 *            the name of the bean property
	 * @param beanClasses
	 *            the classes to check against
	 * @return the property type, or <code>Object.class</code> as fallback
	 */
	public static Class findPropertyType(String propertyName,
			Class[] beanClasses) {
		if (beanClasses != null) {
			for (int i = 0; i < beanClasses.length; i++) {
				PropertyDescriptor pd = getPropertyDescriptor(beanClasses[i],
						propertyName);
				if (pd != null) {
					return pd.getPropertyType();
				}
			}
		}
		return Object.class;
	}

	/**
	 * Check if the given type represents a "simple" property: a primitive, a
	 * String, a Class, or a corresponding array.
	 * <p>
	 * Used to determine properties to check for a "simple" dependency-check.
	 * 
	 * @param clazz
	 *            the type to check
	 * @return whether the given type represent a "simple" property
	 * @see org.springframework.beans.factory.support.RootBeanDefinition#DEPENDENCY_CHECK_SIMPLE
	 * @see org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#checkDependencies
	 */
	public static boolean isSimpleProperty(Class clazz) {
		Assert.notNull(clazz, "Class must not be null");
		return clazz.isPrimitive() || ClassUtils.isPrimitiveArray(clazz)
				|| ClassUtils.isPrimitiveWrapper(clazz)
				|| ClassUtils.isPrimitiveWrapperArray(clazz)
				|| clazz.equals(String.class) || clazz.equals(String[].class)
				|| clazz.equals(Class.class) || clazz.equals(Class[].class);
	}

	/**
	 * Determine if the given target type is assignable from the given value
	 * type, assuming setting by reflection. Considers primitive wrapper classes
	 * as assignable to the corresponding primitive types.
	 * 
	 * @param targetType
	 *            the target type
	 * @param valueType
	 *            the value type that should be assigned to the target type
	 * @return if the target type is assignable from the value type
	 * @deprecated as of Spring 2.0, in favor of
	 *             <code>ClassUtils.isAssignable</code>
	 * @see org.springframework.util.ClassUtils#isAssignable(Class, Class)
	 */
	public static boolean isAssignable(Class targetType, Class valueType) {
		return ClassUtils.isAssignable(targetType, valueType);
	}

	/**
	 * Determine if the given type is assignable from the given value, assuming
	 * setting by reflection. Considers primitive wrapper classes as assignable
	 * to the corresponding primitive types.
	 * 
	 * @param type
	 *            the target type
	 * @param value
	 *            the value that should be assigned to the type
	 * @return if the type is assignable from the value
	 * @deprecated as of Spring 2.0, in favor of
	 *             <code>ClassUtils.isAssignableValue</code>
	 * @see org.springframework.util.ClassUtils#isAssignableValue(Class, Object)
	 */
	public static boolean isAssignable(Class type, Object value) {
		return ClassUtils.isAssignableValue(type, value);
	}

	/**
	 * Copy the property values of the given source bean into the target bean.
	 * <p>
	 * Note: The source and target classes do not have to match or even be
	 * derived from each other, as long as the properties match. Any bean
	 * properties that the source bean exposes but the target bean does not will
	 * silently be ignored.
	 * <p>
	 * This is just a convenience method. For more complex transfer needs,
	 * consider using a full BeanWrapper.
	 * 
	 * @param source
	 *            the source bean
	 * @param target
	 *            the target bean
	 * @throws BeansException
	 *             if the copying failed
	 * @see BeanWrapper
	 */
	public static void copyProperties(Object source, Object target)
			throws BeansException {
		copyProperties(source, target, null, null);
	}

	/**
	 * Copy the property values of the given source bean into the given target
	 * bean, only setting properties defined in the given "editable" class (or
	 * interface).
	 * <p>
	 * Note: The source and target classes do not have to match or even be
	 * derived from each other, as long as the properties match. Any bean
	 * properties that the source bean exposes but the target bean does not will
	 * silently be ignored.
	 * <p>
	 * This is just a convenience method. For more complex transfer needs,
	 * consider using a full BeanWrapper.
	 * 
	 * @param source
	 *            the source bean
	 * @param target
	 *            the target bean
	 * @param editable
	 *            the class (or interface) to restrict property setting to
	 * @throws BeansException
	 *             if the copying failed
	 * @see BeanWrapper
	 */
	public static void copyProperties(Object source, Object target,
			Class editable) throws BeansException {

		copyProperties(source, target, editable, null);
	}

	/**
	 * Copy the property values of the given source bean into the given target
	 * bean, ignoring the given "ignoreProperties".
	 * <p>
	 * Note: The source and target classes do not have to match or even be
	 * derived from each other, as long as the properties match. Any bean
	 * properties that the source bean exposes but the target bean does not will
	 * silently be ignored.
	 * <p>
	 * This is just a convenience method. For more complex transfer needs,
	 * consider using a full BeanWrapper.
	 * 
	 * @param source
	 *            the source bean
	 * @param target
	 *            the target bean
	 * @param ignoreProperties
	 *            array of property names to ignore
	 * @throws BeansException
	 *             if the copying failed
	 * @see BeanWrapper
	 */
	public static void copyProperties(Object source, Object target,
			String[] ignoreProperties) throws BeansException {

		copyProperties(source, target, null, ignoreProperties);
	}

	/**
	 * Copy the property values of the given source bean into the given target
	 * bean.
	 * <p>
	 * Note: The source and target classes do not have to match or even be
	 * derived from each other, as long as the properties match. Any bean
	 * properties that the source bean exposes but the target bean does not will
	 * silently be ignored.
	 * 
	 * @param source
	 *            the source bean
	 * @param target
	 *            the target bean
	 * @param editable
	 *            the class (or interface) to restrict property setting to
	 * @param ignoreProperties
	 *            array of property names to ignore
	 * @throws BeansException
	 *             if the copying failed
	 * @see BeanWrapper
	 */
	private static void copyProperties(Object source, Object target,
			Class editable, String[] ignoreProperties) throws BeansException {

		Assert.notNull(source, "Source must not be null");
		Assert.notNull(target, "Target must not be null");

		Class actualEditable = target.getClass();
		if (editable != null) {
			if (!editable.isInstance(target)) {
				throw new IllegalArgumentException("Target class ["
						+ target.getClass().getName()
						+ "] not assignable to Editable class ["
						+ editable.getName() + "]");
			}
			actualEditable = editable;
		}
		PropertyDescriptor[] targetPds = getPropertyDescriptors(actualEditable);
		List ignoreList = (ignoreProperties != null) ? Arrays
				.asList(ignoreProperties) : null;

		for (int i = 0; i < targetPds.length; i++) {
			PropertyDescriptor targetPd = targetPds[i];
			if (targetPd.getWriteMethod() != null
					&& (ignoreProperties == null || (!ignoreList
							.contains(targetPd.getName())))) {
				PropertyDescriptor sourcePd = getPropertyDescriptor(
						source.getClass(), targetPd.getName());
				if (sourcePd != null && sourcePd.getReadMethod() != null) {
					try {
						Method readMethod = sourcePd.getReadMethod();
						if (!Modifier.isPublic(readMethod.getDeclaringClass()
								.getModifiers())) {
							readMethod.setAccessible(true);
						}
						Object value = readMethod.invoke(source, new Object[0]);
						Method writeMethod = targetPd.getWriteMethod();
						if (!Modifier.isPublic(writeMethod.getDeclaringClass()
								.getModifiers())) {
							writeMethod.setAccessible(true);
						}
						writeMethod.invoke(target, new Object[] { value });
					} catch (Throwable ex) {
						throw new FatalBeanException(
								"Could not copy properties from source to target",
								ex);
					}
				}
			}
		}
	}

}
